前言
由于笔者对React的了解不深,即便算是学习React的时间,到目前也才刚刚半年,所以错误不足之处还望指正。以下都是基于React 15(可能有些是16),webpack1进行探讨(注:未学习过Vue,Ng,Ember,Cycle,Immutable,Redux-Saga,Mobx,Observable,Rxjs等等,所以可能有些方面已经被提及或者解决了,希望不要介意)。
正文
本文的排版可能不那么正经,想到哪写到哪,见谅见谅。
组件
-
JSX
- 备受关注(吐槽)的if问题: 要讨论这个问题,首先要明白jsx是转换的是什么,只有这样,你才明白为什么不能在jsx里面写if语句,switch语句,为什么只能写表达式等等。好了,回到正题,我们困挠,那么其他的程序猿肯定也是这样滴,那么正确的姿势是什么呢?没错,提isssue&讨论,我们先来看看已有的提出的一些方案(来自JSX的issue,其他的诸如三目,把逻辑提取出来写在外面,多return方式,IIFE,do expression等等这里就不多说了):
-
NO.1
https://github.com/facebook/jsx/issues/65#issuecomment-254056396 <if {myCondition}> <div>This part only gets shown if myCondition is true</div> </if>
-
NO.2
https://github.com/facebook/jsx/issues/65#issuecomment-255465492(官方人员提出的,所以采用的概率稍微要高一点,基于do expression) <div> Hi! {if (bool) <MyComponent /> else <div></div>} </div
-
NO.3
https://github.com/facebook/jsx/issues/65#issuecomment-255484351 <If condition={ condition } then={ <component /> } else={ <component /> } /> function If(props) { const condition = props.condition || false; const positive = props['then'] || null; const negative = props['else'] || null; return condition ? positive : negative; }
- 当然,新的语法就意味着新的符号(一般来说),所以这种变化不一定是好的,也不一定每个人都能接受。
- 比如bjrmatos说的
"It's just JavaScript, not a template language" -> no need to replicate JS functionalities with custom syntax. That is the main benefit of JSX IMO, seriously is so easy to do this with js even if it looks "weird" (for me it is not weird, it is just the syntax of the language)
-
比如jmar777说的:
[if (condition)] <Foo /> <Bar /> <Baz /> [/if]
We instead need:
{ condition && <Foo /> } { condition && <Bar /> } { condition && <Baz /> }
If the condition itself gets more complex, then the code gets uglier, or alternatively we need to store the result to a temporary variable, which further increases the verbosity. I will note that this particular pain point would be eased by better support for fragments.
TBH I've never seen a specific proposal that I'm overly fond of, but inasmuch as branching logic is integral to all but the most trivial of rendered outputs, I think there's a compelling case to be made for first-class support in JSX.
- Atrribute autocomplete(这也是我们的愿望之一,毕竟可以减少很多无用功,代码也更整洁)
render () {
const {
prop1,
prop2,
prop3
} = this.props
return (
<MyComponent prop1 prop2 prop3 />
)
}
Or
render () {
const {
prop1,
prop2,
prop3
} = this.props
return (
<MyComponent {prop1} {prop2} {prop3} />
)
}
Instead Of
render () {
const {
prop1,
prop2,
prop3
} = this.props
return (
<MyComponent prop1={prop1} prop2={prop2} prop3={prop3} />
)
}
希望支持数字attribute省略花括号
还有的人希望对于字符串类型的attribute,能够省略引号。seb最后也解释了为什么没有允许可以对字符串属性进行简写(因为存在两种不同的语义,如果允许去掉,到底以哪个为准呢)
https://github.com/facebook/jsx/issues/25#issuecomment-137224657
I think the conclusion is that since these forms are currently semantically different in JSX parsers:
<input value="Foo & Bar" /> // Foo & Bar
<input value={"Foo & Bar"} /> // Foo & Bar
We should keep it consistent which would mean that this would also be different:
<input value=`Foo & Bar` /> // Foo & Bar
<input value={`Foo & Bar`} /> // Foo & Bar
- Attribute Destructuring
https://github.com/facebook/jsx/issues/76
<Foo {fizz, buzz}={bar()} />
// 当然也可以先解构了再赋过去,不过那样肯定是要麻烦一点。最后seb也说了,如果https://github.com/RReverser/es-borrowed-props这个提案有进展的话就更好了
-
Attribute Or Children
- 写过组件的同学应该有时候会遇到这样的情况,我们需要调用方传过来一个组件,那么问题来了,这个组件到底是以children的形式还是props的形式传过来呢?有时候我们只需要一个组件,那么都可以(当然还有考虑语义和大家的惯性思维)。如果需要多个组件就不行了,就需要进行抉择,有时候明明觉得都该以children的形式传进来,但是无奈必须做出取舍。所以我们可以看到比如有些同学就提出了:
- NO.1 attribute2Children模式
https://github.com/facebook/jsx/issues/65#issuecomment-254060186
<SplitContainer
left={<div>...</div>}
right={<div>...</div>}
/>
// vs
<SplitContainer>
<.left>
<div>...</div>
</.left>
<.right>
<div>...</div>
</.right>
</SplitContainer>
NO.2 style(其实也是attribute2Children模式)
https://github.com/facebook/jsx/issues/65#issuecomment-254100455
<style>{`
.your-css-here {color: #333;}
`}</style>
// or
<style>{component_style}</style>
- 关于bind的问题,目前都基本用class里用箭头函数的方式了。但是依然存在一个问题,在某些情况下我们依然需要使用bind(当然用闭包也可以,不过面临的问题是一样的)。比如,我们想给我们的处理函数传递参数,我们就不得不使用诸如
<Foo bar={this.handleSomething.bind(null, arg)} />
的方式,但是众所周知,这样会造成Foo
组件的props每次都会变化,对于PureCompoennt可能有性能影响,(虽然如果这里不是bar
而是ref
的话不会存在这样的问题,因为浅比较的时候根本不会考虑key
和ref
),当然我们可以自己定义shouldComponentUpdate
然后自己手动比较下,如果nextProps
的arg
和现在的arg
一样的话(当然这里会根据arg
是否是对象比较的策略会不一样),那么我们就采用之前的函数(假设我们缓存了)。
-
这样一看似乎还行,但是仍然存在一个问题(其实我们完全没有必要考虑这些,因为对性能真的没啥影响,性能基本不会通过这些得到改善或者降低,但是本着深挖洞,广积粮的精神,思考思考还是可以的)。什么问题呢,就是如果这个
arg
是一个函数或者说是包含函数的对象,那么两个函数相等就能够推导出他们真的相等吗?或者说两个函数不相等就能推导出他们真的不相等吗?显然不能这样,因为函数可能“不纯”。这就给我们的判断工作带来了一定的影响,之前没有出错过是因为出错的几率本身就很低,因为props和state本身都是“纯的”,没有人会去手动修改它(即直接赋值)。我们不那么认真的来看一下下面的例子:class Bar extends Component { shouldComponentUpdate (nextProps, nextState) { if (this.props !== nextProps) { // 问题就出在这里,假设此后我们需要调用p2 if (nextProps.p2 !== this.props.p2) { // 那么这里到底该返回true还是false呢 // 返回true,那如果p2()的值没变怎么办,算不算是一种浪费 // 返回false,那如果p2()的值变了怎么办 } else { // 这里p2相等了,我们能在之后调用之前缓存的p2吗(假设缓存过了,当然缓存的前提一般都是pure的才能缓存哈)?也不能,因为p2可能从一个“纯”的变成“不纯”的了。另外返回false还是true和上面的同理。 } // 所以最后我们会发现,我们根本没法判断,因为我们不知道p2到底“纯”还是“不纯” } } render () { console.log(...this.props); return null; } } let externalData1 = 'extenalData1'; setTimeout(() => externalData1 = 'externalData2', 1000); let util = (...args) => utilFunc(...args, externalData1); class Foo extends Component { state = { arg1: '1', arg2: util('aa') } componentDidMount () { this.setState({ arg1: '2', arg2: util('bb') }); } handleSomething (...args) { console.log(...args); ...somethingElse } render () { const { arg1, arg2 } = this.state; return ( <Bar p1="p1" p2={this.handleSomthing.bind(null, arg1, arg2)} /> ); } }
-
组件
- 一般来说,我们将组件分为两大类,即基础组件与业务组件。在React中,基础组件基本已由Ant Design(PC端)覆盖完毕,将来可能会有一些变动/更新,但是都不会太大。基于基础组件的封装/改写,依旧属于基础组件。业务组件基于基础组件,此时就面临一个非常重要的问题,数据/数据流相关的处理(这个后面再谈)。除此之外,主要想提及一下,还应该有一类组件,暂且称之为逻辑组件,为什么要分出来呢,因为确实感觉这两类都不太能准确的描述它。比如,
<If>
、<Visible mode="null | opacity | visibility">
、<Auth>
(或者<OnlyShowWhenIsAdmin>
之类的)我们的系统基本就是由这三种组件进行拼装。 表单。
- 我们从最简单的说起。一个input,不包含任何联动。那么现在存在一个问题,就是每一个input我们都要建立与之对应的onChange函数(假设我们每一个input都采用Controlled Components的形式,因为可控性要好一点。同时都有自己的逻辑,不单单是
setState(e.target.value)
),太麻烦了。理想的情况是怎么样的呢,我们应该可以采取动态建立的方式,只需要提供字段名,自动将值注入到state中,以及自动在组件的this或者原型链上创建对应的onChange函数,然后再赋给input(当然这样的话基本上要完全依赖提供的组件库,不能期望自己随便写一个input也能自动达到这样的效果)。
- 我们从最简单的说起。一个input,不包含任何联动。那么现在存在一个问题,就是每一个input我们都要建立与之对应的onChange函数(假设我们每一个input都采用Controlled Components的形式,因为可控性要好一点。同时都有自己的逻辑,不单单是
- 那么问题来了,这里需要加糖(使代码更少)和defineProperty(或者Proxy)(使修改更直接方便)吗,个人认为,都不可。因为这都会增加debug的难度。我还是更倾向于前面提到的简单封装,然后还是用setState去改变字段。现在流行的主要有antd的form以及redux-form。但是个人认为这并不是最好的方式,因为感觉有点乱,举个比喻的话,感觉有点像写传统的模板一样,当然,也许就是要专门治治我们这些处女座。
- 下面说说联动的情况。在以前(如jQuery),我们要处理联动,必须手动维护联动的关系,所以要么在onChange里面根据逻辑手动触发其他组件的更新,要么是采用
after
或者callback queue
的方式,总之,重心是维护逻辑而不是数据。那么是否应该存在一种方式,让我们的重心靠到数据上面?这样debug,开发,维护都轻松一些。对于React应用而言,天然的解决了部分问题。但还存在一些问题,比如,单向数据流导致的有时数据链过长过繁琐(所以才产生了redux
),需要在多地保存同一份数据等等。
- 以上仅仅是一些思考,关于表单的探索还没有开始(因为目前表单相关的需求还不是很多和复杂,过段时间研究研究之后希望能解决一些问题)。
- 一般来说,我们将组件分为两大类,即基础组件与业务组件。在React中,基础组件基本已由Ant Design(PC端)覆盖完毕,将来可能会有一些变动/更新,但是都不会太大。基于基础组件的封装/改写,依旧属于基础组件。业务组件基于基础组件,此时就面临一个非常重要的问题,数据/数据流相关的处理(这个后面再谈)。除此之外,主要想提及一下,还应该有一类组件,暂且称之为逻辑组件,为什么要分出来呢,因为确实感觉这两类都不太能准确的描述它。比如,
Encapsulated Compose Or Custom Compose
-
举例来说。例如响应式,我了解到这个概念应该是从bootstrap的栅格开始,到后面antd也有对应的Grid系统,包含了Row和Col。那么问题来了,当实现一个响应式列表或者table的时候,是否应该自己去组合,即哪些地方写个Row,然后下面的Col的sm,md等等依次是多少,挨着填进去。还是说我们先把这些组合逻辑封装好,比如:
const responsive = responsivelizeComponent({ xs: 1, sm: 2, md: 2, lg: 2, pc: 3 });
然后调用,传入数据,以及Row,Col对应的组件:
{ responsive( list, () => <C ... />, // 这里如果C本身就是Row,那么就进行props合并,如果不是Row,那么需要在处理的时候包裹在Row里面 () => <Item ... /> ) // Item同理,只是换成了Col }
- Component Middleware: 我们知道,中间件的概念由来已久了(历史就没有去考证了,node也不熟悉就不谈了哈哈)。在redux中,我们就可以创建一系列的中间件去处理我们的action。那么,组件是否也可以具备这样的能力呢(即组件中间件)?答案是肯定的,decorator就是一个很好的例子,但是decarator与middleware还是存在一定的差距,一是书写上不直观/美观(目前个人能忍受的就是最多1个@),二是控制的力度太小。三是没有形成一个整体,各个部分只能跟自己的上游打交道。这些方面才刚刚开始探索,就不在大佬们面前介(zhuang)绍(bi)了。
数据流
-
Redux
-
细粒度的actionType:
- 目前来说,我们的action的type甚至是action还是设计得太过简单。我们完全可以进一步进行设计来减少我们的工作量。比如,我们可以看到redux初始化store时的actionType为
@@INIT
,受此启发,我们完全可以自定义更复杂的actionType甚至action,配置中间件进行处理。比如@@FOO@@BAR/xx_TODO
,或者像官方例子中的那样const CALL_API = symbol('CALL_API'); action = { [CALL_API]: {...} }
,然后建立与之对应的middleware,从而减少reducer的数量。当然这是一个开头,复杂应用应该是存在很多个middleware进行职能分工(没有复杂应用的经验,这里就不多说了)。
- actionType可以与相应的处理的函数的名字形成包含关系,减少switch里的case代码量。比如,在
reducers/foo.js
中,声明了一系列的handlexxxxType
,那么我们可以在foo.js
中import * as all from './foo.js';
,然后创建一个类似于mapActionTypeToHanleFunction
的处理函数。这里就不用在每个case都去调用某个函数了。
- 一次用户操作导致的多个位置的数据变动是否都可以通过接连触发不同的dispatch action来实现?答案是否定的,比如dispatch某个action,目的是删除某个实体,但是同时sotre中其他某个地方存储了这个id,或者说一个ids数组里包含了这个id,那么在这次dispatch完成之后的更新里就会出错,所以我们不能再dispatch一个action去同步的删除这些数据。我们只能在不同的reducer里面监测第一个dispatch的action,然后处理。
- 还有一个就是群里工业聚大大提到的,action实际上和HTTP请求非常相似,能否用处理HTTP请求的方式去规划redux?
- 目前来说,我们的action的type甚至是action还是设计得太过简单。我们完全可以进一步进行设计来减少我们的工作量。比如,我们可以看到redux初始化store时的actionType为
- Redux-Orm:顾名思义,这就是一个ORM库(毕竟这是一个前端的数据库嘛,之前想过redux配合indexDB,后面发现也不是很方便)。使用后的感受是,确实比纯的redux方便一些(废话,不然人家创建这个干嘛)。具体的比较等之后用了mobx,immutable和rxjs之后再放在一起比较吧。
-
React要想触发更新只能采用
setState
(抛开forceUpdate
不谈),所以这就限制了我们修改数据 -> 自动触发所有相关更新
这种操作,其他的应用我们可以记录所以依赖于这个数据的组件,然后修改数据的时候依次触发它们。但React的话要想实现这个就必须绕一个圈子,如redux方案。同时还存在一些弊端,比如:- 一是依赖某个数据的明明只有A,B组件,为什么我还要挨个通知C,D组件,C,D组件明明是依赖于另外的数据的。即便
react-redux
内部进行了很多性能优化来避免不必要的更新(实质也就是尽量确保两点,一是我不需要的数据不应该触发我的更新,二是即便是我需要的数据,没变化的情况下也不应该更新)。因为redux和react-redux是隔离的,redux就是一个数据(包裹/快递)仓库,当有包裹来的时候,他可以在自己内部对包裹进行分类,把包裹放在指定的某个或某些区域(当然数据天然是可copy很多份的,包裹只有一份,就不要纠结这个啦),然后呢,包裹要出库该走仓库的哪个门呢?不清楚。。type
只是仓库内部分类时用于参考的一个东西,对外部并没有作用。
- 所以它只能依次打开仓库的所有门,拿上扩音器大喊,仓库包裹更新啦,快来看看你们那儿要不要搞些事情~,这里其实就存在一个问题,仓库里哪块区域的包裹更新了其实仓库本身是可以知道的,但是目前他没有做记录,这就导致了收到通知之后的前来的公司又必须亲自去仓库里找自己需要的包裹(
selector
),与此同时,仓库也不能保证这批包裹属于这一次包裹更新中的包裹(即selector
出来的并没有发生变化)。所以这就造成了资源的浪费,有可能一次dispatch
最后仓库里没有任何包裹更新,仓库也必须挨个打开出库门。二就是前面提到的有更新也不应该挨个打开出库门。
- 因此,可能地更好的一种方式是。组件应该把自身可能需要的包裹提前告诉仓库,细粒度一点的,当然既可以是就在
type
和包裹间建立一种映射关系,如{type: { name: 'update_todo', from: this, need: ['foo', 'bar'] }}
(foo
和bar
是store中的某个key), 粗粒度的话(有些case可能不能这样做),就不自己去添need
参数了(但是还是要写from
),可以直接让redux本身进行统计,因为一个action导致了哪些reducer发生了变化这个它是能够统计的(也不难,很早的版本中在combineReducer中就有hasChanged的flag了
),最后也能形成一个type
到need
的映射。然后组件也把need参数传给connect
,同时,redux
内部的listeners就不应再是一个简单的回调数组了,需要按need进行归类,这样我们才能保证不是去通知每个subscribe了的组件,而是确实需要这次更新的我们才通知。还有就是,既然有了need
,也不再需要selector了,我们在通知的时候就自动在store中把对应的need
传过去了。(这里需要注意到是,对于实体数据,store存储的数据肯定还是要normalize后的,不然数据冗余很严重,但是我们通知的时候没有必要再denomalize解范式化或者说像传统的挨个把外键转换为实体,我们直接建立一个reducer存储当前的dispatch对应的网络请求的response)
- 另外这些方面徐飞叔叔的文章真的是写得非常非常不错,自己的想法很多时候就是小巫见大巫了,之后一定还是要抽空多琢磨几遍。
- 一是依赖某个数据的明明只有A,B组件,为什么我还要挨个通知C,D组件,C,D组件明明是依赖于另外的数据的。即便
-
CSS Modules
- 全局与局部: 从整个项目来讲,可以将第三方非CSS Modules模块的css统一放在一个目录下,不做CSS Modules处理。局部的话用
global
语法就行了。
- 模块复用: 比如有两个css模块,
a.css
和b.css
,我们知道,composes
是给对应的标签的class
加上某个css类,而不是像传统的使两个类都具备同样的规则。对于在业务组件中composed
基础组件这样还好,但是如果是composes
其他的业务组件,就会显得有点怪怪的,比如<div class="a-foo b-bar">ssss</div>
,明明是一个和a组件相关的div,却不得不打上b的烙印。同时,我们还不能只用b-bar
,哪怕我们的a-foo
没有东西,也必须写成.a { composes: b-bar from 'b.css' }
才能形成复用。而我们又不太可能单独把b-bar
提取出来作为一个公共组件。
- 命名: 模块化能一定程度上减少长命名,但是无法完全消除长命名。因为缺少需要它。比如一个组件内的一个list,依然需要写成
list
,list-item
,相比以前,省去了xx-list
中的xx
。但有些情况下,比如item下面的内容很少,就一两个,我们不想单独提取出来,那么就得写成list-item-xxx
,确实看着有一点不美观。
- Css的模块是否应和js模块耦合? 通常来说,我们一个css模块会对应一个js模块(除去皮肤,改版这些)。但是除此之外,是否应该存在单独的CSS模块,它们不属于具体的某个基础组件或者业务组件,而是共享给这些组件去使用或者说组合。目前这方面没有实践过,就不多说了。
-
Css Modules与React结合
- 目前已有的方案有react-css-modules以及babel-plugin-react-css-modules,它们的作者是同一个人,后者是前者的改进版本,这位作者也是medium上那篇大家熟悉的stoping css in js的作者。
- 前者(react-css-modules)存在的问题有,一是性能,因为采用的是HOC的模式,导致在生产环境每次渲染也要走一遍检索css映射表的过程,造成性能损耗。二是书写方式麻烦,即便用了decorator,也还是要老是重复同样的代码。三是某些情况下要在一个组件内使用多次HOC,整洁性和方便性都不太好。这些在README中基本都有提到。还有就是不能写空的css规则,内部对此会抛出异常(之前被这个坑过,刚好那个时候的chrome版本吞错,找了很久才发现是这里的问题)。后面跟作者交流了一下,提出了空的css规则的适用场景和初衷,所以在babel-plugin-css-modules中对于此采用警告而不再是抛出异常。
- 后者(babel-plugin-react-css-modules)基本上解决了上述提到的所有问题。但是还有一个地方值得思考一下,那就是import机制的问题,个人认为还需要增加一种特性(可配置对于每个js模块是否启用),即import多个css也不需要指定importName,默认后面的会覆盖前面引入的css中的同名class,否则我们还是要写成
foo.a bar.b
的形式,丧失了我们使用这个的部分初衷。
- 另外采用这样方式暂时有个缺点就是IDE支持不好(没有自动补全提示),尽管7月份发布的最新版webstorm对原生的CSS Modules支持度更好了(
style.
这种方式后会有自动补全的提示)。
- 目前已有的方案有react-css-modules以及babel-plugin-react-css-modules,它们的作者是同一个人,后者是前者的改进版本,这位作者也是medium上那篇大家熟悉的stoping css in js的作者。
其他
- npm是否应该具备更强的约束?即语义版本号必须满足语义所赋予的条件,从而让打包的时候不会1.0.1和1.0.2都打包,应该只打包1.0.2,那个依赖1.0.1的包转而去依赖1.0.2,从而使得只打包一个。更有想法的是,上传到npm上的包必须提供类似react-codemod的机制(这个确实很难,不知深度学习能否有帮助),从而可以让所有使用这个包的应用无痛自动升级,从而实现哪怕是有breaking change的版本更新,比如1.0.2变成1.1.0,最后也只会打包1.1.0。之前包括现在,这部分内容都是通过changelog或者官网更新的方式发放出来。然后让开发者自己去解决。
- 一个目录下有多个js文件,此时一般会建立一个index目录去导出这个目录下所有js里的default以及普通export的变量(当然前提是没有重复的),目的是为了import的时候直接import index文件,避免去记忆某个变量在哪个js文件中。但是这样有一个麻烦,那就是每次新增js文件的时候都需要记得去index进行export。那么我们为什么要南辕北辙的去搞一个index文件呢,我们的目的不就是要import方便吗,为什么不让编译器(其实webpack的alias也可以,但是毕竟是第三方,包括fb内部也有自己的模块导入系统,但是弊端他们也说了)去自动寻找包含我们要引入的那个变量都有哪些js文件然后给我们一个清单呢,就像JAVA的import机制那样,按ctrl+shift+o自动import,如有重复的会让你选择到底以哪个文件导出的为准(当然也有些特殊情况,比如我们需要用as取别名)。
- 越复杂的项目,粒度越小,耦合度越低的项目,组件的数量会成倍的增加,如fb就有3W个组件。那么,我们如何判断某个组件我们或者别人或者团队之前是不是写过,如果是,我们又如何知道这个组件的名字是什么,在哪个位置?(fb虽然有fbjs是开源的,但是那个有点像util,而不是component库,所以也不知道内部到底怎么搞的,之前也只是问过对于react源码他们是不是有比较方便的管理工具,能够快速定位某个功能或者名字或者注释在哪个地方,得到的答案是,没有)。
错误处理(16beta昨天刚发布了,官方有文档了,就看官方的啦),构建,测试,体积优化,部署以后再谈了(其实是因为没什么经验)。嗯,这次就先这样吧
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。